
from django import forms, template
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms.formsets import all_valid
from django.forms.models import BaseInlineFormSet, modelform_factory, modelformset_factory, inlineformset_factory, modelformset_factory, modelform_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode
from django.utils.functional import curry, update_wrapper
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _, ugettext_lazy

from xadmin.options.base import BaseModelAdmin, HORIZONTAL, VERTICAL
from xadmin.options.inlines import *
from xadmin.options.strategies import *
from xadmin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict

from xadmin.xbuttons import *
import logging




LOGGER = logging.getLogger('xadmin.xamodeladmin')


class StrategyManagedClass(forms.MediaDefiningClass):
    def __new__(cls, name, bases, attrs):        
        import inspect    
        super_new = super(StrategyManagedClass, cls).__new__
        module = attrs.pop('__module__')
        new_class = super_new(cls, name, bases, {'__module__': module})
        new_class._strategies = []
        
        s = { 'enable_permissions' : PermissionStrategy(),  
              'enable_services'    : ServicesStrategy(),
              'enable_logging'     : LoggerStrategy(),
              'enable_change'      : ChangeStrategy(),
              'enable_add'         : AddStrategy(),
              'enable_delete'      : DeleteStrategy(),
             }
        
        for k,v in s.items():
            attrs[k] =  attrs.setdefault(k, v)
            new_class._strategies.append( k )
                
        for obj_name, obj in attrs.items():
            new_class.add_to_class(obj_name, obj)
        
        return new_class

    def add_to_class(cls, name, value):
        if hasattr(value, 'contribute_to_class'):
            value.contribute_to_class(cls, name)
        else:
            setattr(cls, name, value)   
            
class ViewProperty(object):
 
    def __init__(self, action, fget=None, doc=None):
        self.action = action
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        return obj._get_view_name(self.action)
      

class IncorrectLookupParameters(Exception):
    pass

class ActionForm(forms.Form):
    action = forms.ChoiceField(label=_('Action:'))
        
class XModelAdmin(BaseModelAdmin):

    __metaclass__ = StrategyManagedClass
    
    list_display = ('__str__',)
    list_display_links = ()
    list_commands = ()
    list_filter = ()
    list_select_related = False
    list_per_page = 100
    list_editable = ()
    change_list_template = None
    
    buttons = ()
        
    search_fields = ()
    date_hierarchy = None
    ordering = None

    # Actions
    actions = []
    action_form = ActionForm
    actions_on_top = True
    actions_on_bottom = False

    # Inlines
    inlines = []

    #templates
    delete_confirmation_template = None
    
    from xadmin.views.main import ChangeList, ERROR_FLAG
    change_list_class = ChangeList
    
    list_view_name = ViewProperty('changelist')
    history_view_name = ViewProperty('history')
    change_view_name = ViewProperty('change')
    add_view_name = ViewProperty('add')
    delete_view_name = ViewProperty('delete')
    op_view_name = ViewProperty('op')
        
    def __init__(self, model, admin_site):
        self.__mode = XModelAdmin._MODE_UNKNOW    
        self.model = model
        self.opts = model._meta
        self.admin_site = admin_site
        
        self.inline_instances = []
        for inline_class in self.inlines:
            inline_instance = inline_class(self.model, self.admin_site)
            self.inline_instances.append(inline_instance)
        
        if 'action_checkbox' not in self.list_display and self.actions is not None:
            self.list_display = ['action_checkbox'] +  list(self.list_display)
        
        if not self.list_display_links:
            for name in self.list_display:
                if name != 'action_checkbox':
                    self.list_display_links = [name]
                    break
        
        super(XModelAdmin, self).__init__()
    
    
    def _get_view_name(self, action):
        def wrap():
            info = self.model._meta.app_label, self.model._meta.module_name
            return "%s_%s" % ('%s_%s' % info, action)
        return wrap()
    
    def _wrap_view(self, view):
        def wrapper(*args, **kwargs):
            return self.admin_site.admin_view(view)(*args, **kwargs)
        return update_wrapper(wrapper, view)
                
    # =XAdmin= Updated
    def get_urls(self):
        #LOGGER.debug(self.model)
        from django.conf.urls.defaults import patterns, url
        #from xadmin.util import has_all_attrs
        #info = self.model._meta.app_label, self.model._meta.module_name

        #urlpatterns = self.get_services_urls()

        urlpatterns = patterns('',
            url(r'^$',
                self._wrap_view(self.changelist_view),
                name=self.list_view_name ),
            url(r'^add/$',
                self._wrap_view(self.add_view),
                name=self.add_view_name),
#            url(r'^(.+)/history/$',
#                self._wrap_view(self.history_view),
#                name=self.get_historyview_name()),
#            url(r'^(.+)/delete/$',
#                self._wrap_view(self.delete_view),
#                name=self.get_deleteview_name()),
#            url(r'^(.+)/read/$',
#                self._wrap_view(self.read_view),
#                name=self.get_readview_name()),
            url(r'^(.+)/op/(.+)$',
                self._wrap_view(self.op_view),
                name=self.op_view_name),
            url(r'^(.+)/$',
                self._wrap_view(self.change_view),
                name=self.change_view_name ),
        )
        
        
        for klass in self._strategies:            
            callback = getattr(klass, 'extra_urls', None)
            if callback:
                urlpatterns = callback(self, urlpatterns)
                        
        
        return urlpatterns           
    
    def urls(self):
        return self.get_urls()
    urls = property(urls)
        
    def save_form(self, request, form, change):
        """
        Given a ModelForm return an unsaved instance. ``change`` is True if
        the object is being changed, and False if it's being added.
        """
        return form.save(commit=False)

    def save_model(self, request, obj, form, change):
        """
        Given a model instance save it to the database.
        """
        obj.save()

    def save_formset(self, request, form, formset, change):
        """
        Given an inline formset save it to the database.
        """
        formset.save()
        
    def _wrap_line_command(self, action, label, template=None, **kwargs):
        def wrapped(obj):
            link = "%s/op/%s" % (obj.pk, action)
            return '<a href="%s">%s</a>' % (link, label)

        wrapped.allow_tags = True
        return wrapped
            
    def changelist_view(self, request, extra_context=None):
        "The 'change list' admin view for this model."
        
                
        opts = self.model._meta
        app_label = opts.app_label
        if hasattr(self, 'has_read_permission'):
            if not self.has_read_permission(request, None):
                raise PermissionDenied

        # Check actions to see if any are available on this changelist
        actions = self.get_actions(request)

        # Remove action checkboxes if there aren't any actions available.
        list_display = list(self.list_display)
        if not actions:
            try:
                list_display.remove('action_checkbox')
            except ValueError:
                pass

        for id, cmd in enumerate(self.list_commands):
            func, action, label = self.get_action(cmd)
            list_display.append(self._wrap_line_command(id, label))
        

        try:
            cl = self.change_list_class(request, self.model, list_display, self.list_display_links, self.list_filter,
                self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_editable, self)
        except IncorrectLookupParameters:
            # Wacky lookup parameters were given, so redirect to the main
            # changelist page, without parameters, and pass an 'invalid=1'
            # parameter via the query string. If wacky parameters were given and
            # the 'invalid=1' parameter was already in the query string, something
            # is screwed up with the database, so display an error page.
            if ERROR_FLAG in request.GET.keys():
                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')

        # If the request was POSTed, this might be a bulk action or a bulk edit.
        # Try to look up an action first, but if this isn't an action the POST
        # will fall through to the bulk edit check, below.
        if actions and request.method == 'POST':
            response = self.response_action(request, queryset=cl.get_query_set())
            if response:
                return response

        # If we're allowing changelist editing, we need to construct a formset
        # for the changelist given all the fields to be edited. Then we'll
        # use the formset to validate/process POSTed data.
        formset = cl.formset = None

        # Handle POSTed bulk-edit data.
        if request.method == "POST" and self.list_editable:
            FormSet = self.get_changelist_formset(request)
            formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
            if formset.is_valid():
                changecount = 0
                for form in formset.forms:
                    if form.has_changed():
                        obj = self.save_form(request, form, change=True)
                        self.save_model(request, obj, form, change=True)
                        form.save_m2m()
                        change_msg = self.construct_change_message(request, form, None)
                        self.log_change(request, obj, change_msg)
                        changecount += 1

                if changecount:
                    if changecount == 1:
                        name = force_unicode(opts.verbose_name)
                    else:
                        name = force_unicode(opts.verbose_name_plural)
                    msg = _("%(count)s %(name)s was changed successfully.",
                                    "%(count)s %(name)s were changed successfully.",
                                    changecount) % {'count': changecount,
                                                    'name': name,
                                                    'obj': force_unicode(obj)}
                    self.message_user(request, msg)

                return HttpResponseRedirect(request.get_full_path())

        # Handle GET -- construct a formset for display.
        elif self.list_editable:
            FormSet = self.get_changelist_formset(request)
            formset = cl.formset = FormSet(queryset=cl.result_list)

        # Build the list of media to be used by the formset.
        if formset:
            media = self.media + formset.media
        else:
            media = self.media

        # Build the action form and populate it with available actions.
        if actions:
            action_form = self.action_form(auto_id=None)
            action_form.fields['action'].choices = self.get_action_choices(request)
        else:
            action_form = None

        context = {
            'title': cl.title,
            'is_popup': cl.is_popup,
            'cl': cl,
            'media': media,
            'has_add_permission': self.has_add_permission(request),
            'root_path': self.admin_site.root_path,
            'app_label': app_label,
            'action_form': action_form,
            'actions_on_top': self.actions_on_top,
            'actions_on_bottom': self.actions_on_bottom,
        }
        context.update(extra_context or {})
        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
        return render_to_response(self.change_list_template or [
            'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
            'admin/%s/change_list.html' % app_label,
            'admin/change_list.html'
        ], context, context_instance=context_instance)

    def queryset(self, request):
        """
        Returns a QuerySet of all model instances that can be edited by the
        admin site. This is used by changelist_view.
        """
        qs = self.model._default_manager.get_query_set()
        # TODO: this should be handled by some parameter to the ChangeList.
        ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
        if ordering:
            qs = qs.order_by(*ordering)
        return qs

    def get_changelist_form(self, request, **kwargs):
        """
        Returns a Form class for use in the Formset on the changelist page.
        """
        defaults = {
            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
        }
        defaults.update(kwargs)
        return modelform_factory(self.model, **defaults)
            
    def get_changelist_formset(self, request, **kwargs):
        """
        Returns a FormSet class for use on the changelist page if list_editable
        is used.
        """
        defaults = {
            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
        }
        defaults.update(kwargs)
        return modelformset_factory(self.model,
            self.get_changelist_form(request), extra=0,
            fields=self.list_editable, **defaults)        
        
    def _get_extra_buttons(self, request, object_id=None):
        LOGGER.debug("")
        btns = []
        for id, cmd in enumerate(self.buttons):
            func, action, label = self.get_action(cmd)
            btns.append( XActionButton(id, object_id, label) )
        return btns    
    
    def x_extra_urls(self, urlpatterns):
        from django.conf.urls.defaults import patterns, url
        ret = patterns('',
                                url(r'^(.+)/op/(.+)$',
                                    self._wrap_view(self.op_view),
                                    name=self.get_opview_name()),
                                )
        
        return urlpatterns + ret
     
    def op_view(self, request, object_id, action_index, extra_context=None):
        func = self.list_commands[int(action_index)]
        func, action, description = self.get_action( func )
        import inspect
        args, varargs, varkw, defaults = inspect.getargspec(func)
        params = {'request' : request,
                  'queryset': self.model._default_manager.get_query_set().filter(pk=object_id),
                  'object_id': object_id,
                  'modeladmin': self
                  } 

        if 'self' in args:
            return func(self, **params)
        else:
            return func(**params)        
        
        
    def action_checkbox(self, obj):
        """
        A list_display column containing a checkbox widget.
        """
        from xadmin import helpers
        return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
    action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
    action_checkbox.allow_tags = True

    def get_actions(self, request):
        """
        Return a dictionary mapping the names of all actions for this
        ModelAdmin to a tuple of (callable, name, description) for each action.
        """
        # If self.actions is explicitally set to None that means that we don't
        # want *any* actions enabled on this page.
        if self.actions is None:
            return []

        actions = []

        # Gather actions from the admin site first
        for (name, func) in self.admin_site.actions:
            description = getattr(func, 'short_description', name.replace('_', ' '))
            actions.append((func, name, description))

        # Then gather them from the model admin and all parent classes,
        # starting with self and working back up.
        for klass in self.__class__.mro()[::-1]:
            class_actions = getattr(klass, 'actions', [])
            # Avoid trying to iterate over None
            if not class_actions:
                continue
            actions.extend([self.get_action(action) for action in class_actions])

        # get_action might have returned None, so filter any of those out.
        actions = filter(None, actions)

        # Convert the actions into a SortedDict keyed by name
        # and sorted by description.
        actions.sort(lambda a,b: cmp(a[2].lower(), b[2].lower()))
        actions = SortedDict([
            (name, (func, name, desc))
            for func, name, desc in actions
        ])

        return actions

    def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
        """
        Return a list of choices for use in a form object.  Each choice is a
        tuple (name, description).
        """
        choices = [] + default_choices
        for func, name, description in self.get_actions(request).itervalues():
            choice = (name, description % model_format_dict(self.opts))
            choices.append(choice)
        return choices

    def get_action(self, action):
        """
        Return a given action from a parameter, which can either be a callable,
        or the name of a method on the ModelAdmin.  Return is a tuple of
        (callable, name, description).
        """
        # If the action is a callable, just use it.
        if callable(action):
            func = action
            action = action.__name__

        # Next, look for a method. Grab it off self.__class__ to get an unbound
        # method instead of a bound one; this ensures that the calling
        # conventions are the same for functions and methods.
        elif hasattr(self.__class__, action):
            func = getattr(self.__class__, action)

        # Finally, look for a named method on the admin site
        else:
            try:
                func = self.admin_site.get_action(action)
            except KeyError:
                return None

        if hasattr(func, 'short_description'):
            description = func.short_description
        else:
            description = capfirst(action.replace('_', ' '))
        return func, action, description

    def response_action(self, request, queryset):
        """
        Handle an admin action. This is called if a request is POSTed to the
        changelist; it returns an HttpResponse if the action was handled, and
        None otherwise.
        """
        from xadmin import helpers
        # There can be multiple action forms on the page (at the top
        # and bottom of the change list, for example). Get the action
        # whose button was pushed.
        try:
            action_index = int(request.POST.get('index', 0))
        except ValueError:
            action_index = 0

        # Construct the action form.
        data = request.POST.copy()
        data.pop(helpers.ACTION_CHECKBOX_NAME, None)
        data.pop("index", None)

        # Use the action whose button was pushed
        try:
            data.update({'action': data.getlist('action')[action_index]})
        except IndexError:
            # If we didn't get an action from the chosen form that's invalid
            # POST data, so by deleting action it'll fail the validation check
            # below. So no need to do anything here
            pass

        action_form = self.action_form(data, auto_id=None)
        action_form.fields['action'].choices = self.get_action_choices(request)

        # If the form's valid we can handle the action.
        if action_form.is_valid():
            action = action_form.cleaned_data['action']
            func, name, description = self.get_actions(request)[action]

            # Get the list of selected PKs. If nothing's selected, we can't
            # perform an action on it, so bail.
            selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
            if not selected:
                return None

            response = func(self, request, queryset.filter(pk__in=selected))

            # Actions may return an HttpResponse, which will be used as the
            # response from the POST. If not, we'll be a good little HTTP
            # citizen and redirect back to the changelist page.
            if isinstance(response, HttpResponse):
                return response
            else:
                return HttpResponseRedirect(".")
        
        
        
# delegated
    # permissions
    class HTTP_404(object):
        def __init__(self):
            raise Http404()
        
    has_add_permission = lambda s,r: True
    has_read_permission = lambda s,r,o=None: True
    has_change_permission = lambda s,r,o=None: True
    has_delete_permission = lambda s,r,o=None: True    
    get_model_perms = lambda s,r: {'add': True,'change': True,'delete': True,'read': True}
    
    # logging
    construct_change_message = lambda s,a,b,c: None
    log_addition = log_change = log_deletion = message_user = lambda a,b,c: None
    
    change_view = lambda s,r,o: XModelAdmin.HTTP_404()
    add_view = lambda s,r: XModelAdmin.HTTP_404()
    
    